/* *****************************************************************************
 * Copyright (c) 2017 Evernote Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Ralf Wondratschek - initial version
 *******************************************************************************/
package com.evernote.android.state;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.Compiler;
import com.google.testing.compile.JavaFileObjects;

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import java.util.HashMap;
import java.util.List;

import javax.lang.model.element.Element;
import javax.tools.JavaFileObject;

import static com.google.testing.compile.CompilationSubject.assertThat;
import static org.junit.Assert.assertEquals;

/**
 * @author rwondratschek
 */
@FixMethodOrder(MethodSorters.JVM)
public class TestProcessor {

    private static final String PACKAGE = "com.evernote.android.state.test.";

    private static String getName(String className) {
        return getName(className, false);
    }

    private static String getName(String className, boolean addSuffix) {
        return PACKAGE + className + (addSuffix ? StateProcessor.STATE_SAVER_SUFFIX : "");
    }

    @Test
    public void testSimple() {
        String className = "TestSimple";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestSimple {\n"
                + "    @State\n"
                + "    int field;\n"
                + "}\n");

        JavaFileObject expected = JavaFileObjects.forSourceString(getName(className, true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestSimple$$StateSaver<T extends TestSimple> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestSimple$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putInt(state, \"field\", target.field);\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.field = HELPER.getInt(state, \"field\");\n"
                + "  }\n"
                + "}\n");

        StateProcessor stateProcessor = new StateProcessor();

        Compilation compilation = Compiler.javac().withProcessors(stateProcessor).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected);

        HashMap<String, List<Element>> map = stateProcessor.getMapGeneratedFileToOriginatingElements();

        assertEquals(map.size(), 1);
        assertEquals(compilation.generatedSourceFiles().size(), 1);
    }

    @Test
    public void testProperty() {
        String className = "TestProperty";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestProperty {\n"
                + "    @State\n"
                + "    private int test;\n"
                + "\n"
                + "    public int getTest() {\n"
                + "        return test;\n"
                + "    }\n"
                + "\n"
                + "    public void setTest(int test) {\n"
                + "        this.test = test;\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected = JavaFileObjects.forSourceString(getName(className, true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestProperty$$StateSaver<T extends TestProperty> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestProperty$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putInt(state, \"Test\", target.getTest());\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.setTest(HELPER.getInt(state, \"Test\"));\n"
                + "  }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected);
    }

    @Test
    public void testNested() {
        String className = "TestNested";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestNested {\n"
                + "    @State\n"
                + "    public int test;\n"
                + "\n"
                + "    public static class Inner1 {\n"
                + "        @State\n"
                + "        public int test;\n"
                + "\n"
                + "        public static class InnerInner1 {\n"
                + "            @State\n"
                + "            public int test;\n"
                + "        }\n"
                + "        public static class InnerInner2 {\n"
                + "            @State\n"
                + "            public int test;\n"
                + "        }\n"
                + "    }\n"
                + "    public static class Inner2 {\n"
                + "        @State\n"
                + "        public int test;\n"
                + "\n"
                + "        public static class InnerInner1 {\n"
                + "            @State\n"
                + "            public int test;\n"
                + "        }\n"
                + "        public static class InnerInner2 {\n"
                + "            @State\n"
                + "            public int test;\n"
                + "        }\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected1 = JavaFileObjects.forSourceString(getName("TestNested$Inner1$InnerInner1", true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestNested$Inner1$InnerInner1$$StateSaver<T extends TestNested.Inner1.InnerInner1> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestNested$Inner1$InnerInner1$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putInt(state, \"test\", target.test);\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.test = HELPER.getInt(state, \"test\");\n"
                + "  }\n"
                + "}\n");

        JavaFileObject expected2 = JavaFileObjects.forSourceString(getName("TestNested$Inner2$InnerInner1", true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestNested$Inner2$InnerInner1$$StateSaver<T extends TestNested.Inner2.InnerInner1> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestNested$Inner2$InnerInner1$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putInt(state, \"test\", target.test);\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.test = HELPER.getInt(state, \"test\");\n"
                + "  }\n"
                + "}\n");

        StateProcessor stateProcessor = new StateProcessor();

        Compilation compilation = Compiler.javac().withProcessors(stateProcessor).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName("TestNested$Inner1$InnerInner1", true)).hasSourceEquivalentTo(expected1);
        assertThat(compilation).generatedSourceFile(getName("TestNested$Inner2$InnerInner1", true)).hasSourceEquivalentTo(expected2);

        HashMap<String, List<Element>> map = stateProcessor.getMapGeneratedFileToOriginatingElements();

        assertEquals(map.size(), 7);
        assertEquals(compilation.generatedSourceFiles().size(), 7);
    }

    @Test
    public void testView() {
        String className = "TestView";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.content.Context;\n"
                + "import android.os.Parcelable;\n"
                + "import android.view.View;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "import com.evernote.android.state.StateSaver;\n"
                + "\n"
                + "public class TestView extends View {\n"
                + "\n"
                + "    @State\n"
                + "    public int mState;\n"
                + "\n"
                + "    public TestView(Context context) {\n"
                + "        super(context);\n"
                + "    }\n"
                + "\n"
                + "    @Override\n"
                + "    protected Parcelable onSaveInstanceState() {\n"
                + "        return StateSaver.saveInstanceState(this, super.onSaveInstanceState());\n"
                + "    }\n"
                + "\n"
                + "    @Override\n"
                + "    protected void onRestoreInstanceState(Parcelable state) {\n"
                + "        super.onRestoreInstanceState(StateSaver.restoreInstanceState(this, state));\n"
                + "    }\n"
                + "\n"
                + "    public static class InnerView extends TestView {\n"
                + "\n"
                + "        @State\n"
                + "        public int mStateInner;\n"
                + "\n"
                + "        public InnerView(Context context) {\n"
                + "            super(context);\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        protected Parcelable onSaveInstanceState() {\n"
                + "            return StateSaver.saveInstanceState(this, super.onSaveInstanceState());\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        protected void onRestoreInstanceState(Parcelable state) {\n"
                + "            super.onRestoreInstanceState(StateSaver.restoreInstanceState(this, state));\n"
                + "        }\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected1 = JavaFileObjects.forSourceString(getName(className, true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import android.os.Parcelable;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestView$$StateSaver<T extends TestView> extends Injector.View<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestView$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public Parcelable save(T target, Parcelable p) {\n"
                + "    Bundle state = HELPER.putParent(p);\n"
                + "    HELPER.putInt(state, \"mState\", target.mState);\n"
                + "    return state;\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public Parcelable restore(T target, Parcelable p) {\n"
                + "    Bundle state = (Bundle) p;\n"
                + "    target.mState = HELPER.getInt(state, \"mState\");\n"
                + "    return HELPER.getParent(state);\n"
                + "  }\n"
                + "}\n");

        JavaFileObject expected2 = JavaFileObjects.forSourceString(getName("TestView$InnerView", true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import android.os.Parcelable;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestView$InnerView$$StateSaver<T extends TestView.InnerView> extends TestView$$StateSaver<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestView$InnerView$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public Parcelable save(T target, Parcelable p) {\n"
                + "    Bundle state = HELPER.putParent(super.save(target, p));\n"
                + "    HELPER.putInt(state, \"mStateInner\", target.mStateInner);\n"
                + "    return state;\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public Parcelable restore(T target, Parcelable p) {\n"
                + "    Bundle state = (Bundle) p;\n"
                + "    target.mStateInner = HELPER.getInt(state, \"mStateInner\");\n"
                + "    return super.restore(target, HELPER.getParent(state));\n"
                + "  }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected1);
        assertThat(compilation).generatedSourceFile(getName("TestView$InnerView", true)).hasSourceEquivalentTo(expected2);
    }

    @Test
    public void testBundler() {
        String className = "TestBundler";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestBundler {\n"
                + "\n"
                + "    @State(MyBundler.class)\n"
                + "    private Data mData2;\n"
                + "\n"
                + "    public Data getData2() {\n"
                + "        return mData2;\n"
                + "    }\n"
                + "\n"
                + "    public void setData2(Data data2) {\n"
                + "        mData2 = data2;\n"
                + "    }\n"
                + "\n"
                + "    public static final class Data {\n"
                + "        public int int1;\n"
                + "        public int int2;\n"
                + "    }\n"
                + "\n"
                + "    public static final class MyBundler implements Bundler<Data> {\n"
                + "        @Override\n"
                + "        public void put(String key, Data value, Bundle bundle) {\n"
                + "            if (value != null) {\n"
                + "                bundle.putInt(key + \"1\", value.int1);\n"
                + "                bundle.putInt(key + \"2\", value.int2);\n"
                + "            }\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public Data get(String key, Bundle bundle) {\n"
                + "            if (bundle.containsKey(key + \"1\")) {\n"
                + "                Data data = new Data();\n"
                + "                data.int1 = bundle.getInt(key + \"1\");\n"
                + "                data.int2 = bundle.getInt(key + \"2\");\n"
                + "                return data;\n"
                + "            } else {\n"
                + "                return null;\n"
                + "            }\n"
                + "        }\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected = JavaFileObjects.forSourceString(getName(className, true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestBundler$$StateSaver<T extends TestBundler> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestBundler$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "    BUNDLERS.put(\"Data2\", new TestBundler.MyBundler());\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putWithBundler(state, \"Data2\", target.getData2());\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.setData2(HELPER.<TestBundler.Data>getWithBundler(state, \"Data2\"));\n"
                + "  }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected);
    }

    @Test
    public void testAllTypes() {
        String className = "TestTypes";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import android.os.Parcel;\n"
                + "import android.os.Parcelable;\n"
                + "import android.util.SparseArray;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "import java.io.Serializable;\n"
                + "import java.util.ArrayList;\n"
                + "\n"
                + "public class TestTypes {\n"
                + "    @State\n"
                + "    public boolean mBoolean;\n"
                + "    @State\n"
                + "    public boolean[] mBooleanArray;\n"
                + "    @State\n"
                + "    public Boolean mBooleanObj;\n"
                + "    @State\n"
                + "    public byte mByte;\n"
                + "    @State\n"
                + "    public byte[] mByteArray;\n"
                + "    @State\n"
                + "    public Byte mByteObj;\n"
                + "    @State\n"
                + "    public char mChar;\n"
                + "    @State\n"
                + "    public char[] mCharArray;\n"
                + "    @State\n"
                + "    public Character mCharObj;\n"
                + "    @State\n"
                + "    public double mDouble;\n"
                + "    @State\n"
                + "    public double[] mDoubleArray;\n"
                + "    @State\n"
                + "    public Double mDoubleObj;\n"
                + "    @State\n"
                + "    public float mFloat;\n"
                + "    @State\n"
                + "    public float[] mFloatArray;\n"
                + "    @State\n"
                + "    public Float mFloatObj;\n"
                + "    @State\n"
                + "    public int mInt;\n"
                + "    @State\n"
                + "    public int[] mIntArray;\n"
                + "    @State\n"
                + "    public Integer mIntegerObj;\n"
                + "    @State\n"
                + "    public long mLong;\n"
                + "    @State\n"
                + "    public long[] mLongArray;\n"
                + "    @State\n"
                + "    public Long mLongObj;\n"
                + "    @State\n"
                + "    public short mShort;\n"
                + "    @State\n"
                + "    public short[] mShortArray;\n"
                + "    @State\n"
                + "    public Short mShortObj;\n"
                + "    @State\n"
                + "    public CharSequence mCharSequence;\n"
                + "    @State\n"
                + "    public CharSequence[] mCharSequenceArray;\n"
                + "    @State\n"
                + "    public String mString;\n"
                + "    @State\n"
                + "    public String[] mStringArray;\n"
                + "    @State\n"
                + "    public ArrayList<CharSequence> mCharSequenceArrayList;\n"
                + "    @State\n"
                + "    public ArrayList<Integer> mIntegerArrayList;\n"
                + "    @State\n"
                + "    public ArrayList<String> mStringArrayList;\n"
                + "    @State\n"
                + "    public Bundle mBundle;\n"
                + "    @State\n"
                + "    public Parcelable[] mParcelableArray;\n"
                + "    @State\n"
                + "    public ParcelableImpl mParcelableImpl;\n"
                + "    @State\n"
                + "    public SerializableImpl mSerializableImpl;\n"
                + "    @State\n"
                + "    public ArrayList<ParcelableImpl> mParcelableArrayList;\n"
                + "    @State\n"
                + "    public SparseArray<ParcelableImpl> mParcelableSparseArray;\n"
                + "\n"
                + "    public static class ParcelableImpl implements Parcelable {\n"
                + "        private int mInt;\n"
                + "\n"
                + "        public ParcelableImpl(int anInt) {\n"
                + "            mInt = anInt;\n"
                + "        }\n"
                + "\n"
                + "        protected ParcelableImpl(Parcel in) {\n"
                + "            mInt = in.readInt();\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public boolean equals(Object o) {\n"
                + "            if (this == o) return true;\n"
                + "            if (o == null || getClass() != o.getClass()) return false;\n"
                + "\n"
                + "            ParcelableImpl that = (ParcelableImpl) o;\n"
                + "\n"
                + "            return mInt == that.mInt;\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public int hashCode() {\n"
                + "            return mInt;\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public void writeToParcel(Parcel dest, int flags) {\n"
                + "            dest.writeInt(mInt);\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public int describeContents() {\n"
                + "            return 0;\n"
                + "        }\n"
                + "\n"
                + "        public static final Creator<ParcelableImpl> CREATOR = new Creator<ParcelableImpl>() {\n"
                + "            @Override\n"
                + "            public ParcelableImpl createFromParcel(Parcel in) {\n"
                + "                return new ParcelableImpl(in);\n"
                + "            }\n"
                + "\n"
                + "            @Override\n"
                + "            public ParcelableImpl[] newArray(int size) {\n"
                + "                return new ParcelableImpl[size];\n"
                + "            }\n"
                + "        };\n"
                + "    }\n"
                + "\n"
                + "    public static class SerializableImpl implements Serializable {\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected = JavaFileObjects.forSourceString(getName(className, true), ""
                + "/* *****************************************************************************\n"
                + " * Copyright (c) 2017 Evernote Corporation.\n"
                + " * This software was generated by Evernote’s Android-State code generator\n"
                + " * (available at https://github.com/evernote/android-state), which is made\n"
                + " * available under the terms of the Eclipse Public License v1.0\n"
                + " * (available at http://www.eclipse.org/legal/epl-v10.html).\n"
                + " * For clarification, code generated by the Android-State code generator is\n"
                + " * not subject to the Eclipse Public License and can be used subject to the\n"
                + " * following terms:\n"
                + " *\n"
                + " * Permission is hereby granted, free of charge, to any person obtaining a copy\n"
                + " * of this software and associated documentation files (the \"Software\"), to deal\n"
                + " * in the Software without restriction, including without limitation the rights\n"
                + " * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n"
                + " * copies of the Software, and to permit persons to whom the Software is\n"
                + " * furnished to do so, subject to the following conditions:\n"
                + " *\n"
                + " * The above copyright notice and this permission notice shall be included in all\n"
                + " * copies or substantial portions of the Software.\n"
                + " *\n"
                + " * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
                + " * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
                + " * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
                + " * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n"
                + " * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n"
                + " * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
                + " * SOFTWARE.\n"
                + " *******************************************************************************/\n"
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestTypes$$StateSaver<T extends TestTypes> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestTypes$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putBoolean(state, \"mBoolean\", target.mBoolean);\n"
                + "    HELPER.putBooleanArray(state, \"mBooleanArray\", target.mBooleanArray);\n"
                + "    HELPER.putBoxedBoolean(state, \"mBooleanObj\", target.mBooleanObj);\n"
                + "    HELPER.putBundle(state, \"mBundle\", target.mBundle);\n"
                + "    HELPER.putByte(state, \"mByte\", target.mByte);\n"
                + "    HELPER.putByteArray(state, \"mByteArray\", target.mByteArray);\n"
                + "    HELPER.putBoxedByte(state, \"mByteObj\", target.mByteObj);\n"
                + "    HELPER.putChar(state, \"mChar\", target.mChar);\n"
                + "    HELPER.putCharArray(state, \"mCharArray\", target.mCharArray);\n"
                + "    HELPER.putBoxedChar(state, \"mCharObj\", target.mCharObj);\n"
                + "    HELPER.putCharSequence(state, \"mCharSequence\", target.mCharSequence);\n"
                + "    HELPER.putCharSequenceArray(state, \"mCharSequenceArray\", target.mCharSequenceArray);\n"
                + "    HELPER.putCharSequenceArrayList(state, \"mCharSequenceArrayList\", target.mCharSequenceArrayList);\n"
                + "    HELPER.putDouble(state, \"mDouble\", target.mDouble);\n"
                + "    HELPER.putDoubleArray(state, \"mDoubleArray\", target.mDoubleArray);\n"
                + "    HELPER.putBoxedDouble(state, \"mDoubleObj\", target.mDoubleObj);\n"
                + "    HELPER.putFloat(state, \"mFloat\", target.mFloat);\n"
                + "    HELPER.putFloatArray(state, \"mFloatArray\", target.mFloatArray);\n"
                + "    HELPER.putBoxedFloat(state, \"mFloatObj\", target.mFloatObj);\n"
                + "    HELPER.putInt(state, \"mInt\", target.mInt);\n"
                + "    HELPER.putIntArray(state, \"mIntArray\", target.mIntArray);\n"
                + "    HELPER.putIntegerArrayList(state, \"mIntegerArrayList\", target.mIntegerArrayList);\n"
                + "    HELPER.putBoxedInt(state, \"mIntegerObj\", target.mIntegerObj);\n"
                + "    HELPER.putLong(state, \"mLong\", target.mLong);\n"
                + "    HELPER.putLongArray(state, \"mLongArray\", target.mLongArray);\n"
                + "    HELPER.putBoxedLong(state, \"mLongObj\", target.mLongObj);\n"
                + "    HELPER.putParcelableArray(state, \"mParcelableArray\", target.mParcelableArray);\n"
                + "    HELPER.putParcelableArrayList(state, \"mParcelableArrayList\", target.mParcelableArrayList);\n"
                + "    HELPER.putParcelable(state, \"mParcelableImpl\", target.mParcelableImpl);\n"
                + "    HELPER.putSparseParcelableArray(state, \"mParcelableSparseArray\", target.mParcelableSparseArray);\n"
                + "    HELPER.putSerializable(state, \"mSerializableImpl\", target.mSerializableImpl);\n"
                + "    HELPER.putShort(state, \"mShort\", target.mShort);\n"
                + "    HELPER.putShortArray(state, \"mShortArray\", target.mShortArray);\n"
                + "    HELPER.putBoxedShort(state, \"mShortObj\", target.mShortObj);\n"
                + "    HELPER.putString(state, \"mString\", target.mString);\n"
                + "    HELPER.putStringArray(state, \"mStringArray\", target.mStringArray);\n"
                + "    HELPER.putStringArrayList(state, \"mStringArrayList\", target.mStringArrayList);\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.mBoolean = HELPER.getBoolean(state, \"mBoolean\");\n"
                + "    target.mBooleanArray = HELPER.getBooleanArray(state, \"mBooleanArray\");\n"
                + "    target.mBooleanObj = HELPER.getBoxedBoolean(state, \"mBooleanObj\");\n"
                + "    target.mBundle = HELPER.getBundle(state, \"mBundle\");\n"
                + "    target.mByte = HELPER.getByte(state, \"mByte\");\n"
                + "    target.mByteArray = HELPER.getByteArray(state, \"mByteArray\");\n"
                + "    target.mByteObj = HELPER.getBoxedByte(state, \"mByteObj\");\n"
                + "    target.mChar = HELPER.getChar(state, \"mChar\");\n"
                + "    target.mCharArray = HELPER.getCharArray(state, \"mCharArray\");\n"
                + "    target.mCharObj = HELPER.getBoxedChar(state, \"mCharObj\");\n"
                + "    target.mCharSequence = HELPER.getCharSequence(state, \"mCharSequence\");\n"
                + "    target.mCharSequenceArray = HELPER.getCharSequenceArray(state, \"mCharSequenceArray\");\n"
                + "    target.mCharSequenceArrayList = HELPER.getCharSequenceArrayList(state, \"mCharSequenceArrayList\");\n"
                + "    target.mDouble = HELPER.getDouble(state, \"mDouble\");\n"
                + "    target.mDoubleArray = HELPER.getDoubleArray(state, \"mDoubleArray\");\n"
                + "    target.mDoubleObj = HELPER.getBoxedDouble(state, \"mDoubleObj\");\n"
                + "    target.mFloat = HELPER.getFloat(state, \"mFloat\");\n"
                + "    target.mFloatArray = HELPER.getFloatArray(state, \"mFloatArray\");\n"
                + "    target.mFloatObj = HELPER.getBoxedFloat(state, \"mFloatObj\");\n"
                + "    target.mInt = HELPER.getInt(state, \"mInt\");\n"
                + "    target.mIntArray = HELPER.getIntArray(state, \"mIntArray\");\n"
                + "    target.mIntegerArrayList = HELPER.getIntegerArrayList(state, \"mIntegerArrayList\");\n"
                + "    target.mIntegerObj = HELPER.getBoxedInt(state, \"mIntegerObj\");\n"
                + "    target.mLong = HELPER.getLong(state, \"mLong\");\n"
                + "    target.mLongArray = HELPER.getLongArray(state, \"mLongArray\");\n"
                + "    target.mLongObj = HELPER.getBoxedLong(state, \"mLongObj\");\n"
                + "    target.mParcelableArray = HELPER.getParcelableArray(state, \"mParcelableArray\");\n"
                + "    target.mParcelableArrayList = HELPER.getParcelableArrayList(state, \"mParcelableArrayList\");\n"
                + "    target.mParcelableImpl = HELPER.getParcelable(state, \"mParcelableImpl\");\n"
                + "    target.mParcelableSparseArray = HELPER.getSparseParcelableArray(state, \"mParcelableSparseArray\");\n"
                + "    target.mSerializableImpl = HELPER.getSerializable(state, \"mSerializableImpl\");\n"
                + "    target.mShort = HELPER.getShort(state, \"mShort\");\n"
                + "    target.mShortArray = HELPER.getShortArray(state, \"mShortArray\");\n"
                + "    target.mShortObj = HELPER.getBoxedShort(state, \"mShortObj\");\n"
                + "    target.mString = HELPER.getString(state, \"mString\");\n"
                + "    target.mStringArray = HELPER.getStringArray(state, \"mStringArray\");\n"
                + "    target.mStringArrayList = HELPER.getStringArrayList(state, \"mStringArrayList\");\n"
                + "  }\n"
                + "}");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected);
    }

    @Test
    public void testReflection() {
        String className = "TestTypesReflection";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import android.os.Parcelable;\n"
                + "import android.util.SparseArray;\n"
                + "\n"
                + "import com.evernote.android.state.StateReflection;\n"
                + "\n"
                + "import java.util.ArrayList;\n"
                + "\n"
                + "public class TestTypesReflection {\n"
                + "    @StateReflection\n"
                + "    private int mInt;\n"
                + "    @StateReflection\n"
                + "    private int[] mIntArray;\n"
                + "    @StateReflection\n"
                + "    private Integer mIntegerObj;\n"
                + "    @StateReflection\n"
                + "    private Bundle mBundle;\n"
                + "    @StateReflection\n"
                + "    private Parcelable[] mParcelableArray;\n"
                + "    @StateReflection\n"
                + "    private ArrayList<? extends TestTypes.ParcelableImpl> mParcelableArrayList;\n"
                + "    @StateReflection\n"
                + "    private SparseArray<TestTypes.ParcelableImpl> mParcelableSparseArray;\n"
                + "\n"
                + "    public int getInt() {\n"
                + "        return mInt;\n"
                + "    }\n"
                + "\n"
                + "    public void setInt(int anInt) {\n"
                + "        mInt = anInt;\n"
                + "    }\n"
                + "\n"
                + "    public Integer getIntegerObj() {\n"
                + "        return mIntegerObj;\n"
                + "    }\n"
                + "\n"
                + "    public void setIntegerObj(Integer integerObj) {\n"
                + "        mIntegerObj = integerObj;\n"
                + "    }\n"
                + "\n"
                + "    public ArrayList<? extends TestTypes.ParcelableImpl> getParcelableArrayList() {\n"
                + "        return mParcelableArrayList;\n"
                + "    }\n"
                + "\n"
                + "    public void setParcelableArrayList(ArrayList<? extends TestTypes.ParcelableImpl> parcelableArrayList) {\n"
                + "        mParcelableArrayList = parcelableArrayList;\n"
                + "    }\n"
                + "}\n");

        JavaFileObject javaFileObjectInnerClass = JavaFileObjects.forSourceString(getName("TestTypes"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import android.os.Parcel;\n"
                + "import android.os.Parcelable;\n"
                + "import android.util.SparseArray;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "import java.io.Serializable;\n"
                + "import java.util.ArrayList;\n"
                + "\n"
                + "public class TestTypes {\n"
                + "    @State\n"
                + "    public boolean mBoolean;\n"
                + "    @State\n"
                + "    public boolean[] mBooleanArray;\n"
                + "    @State\n"
                + "    public Boolean mBooleanObj;\n"
                + "    @State\n"
                + "    public byte mByte;\n"
                + "    @State\n"
                + "    public byte[] mByteArray;\n"
                + "    @State\n"
                + "    public Byte mByteObj;\n"
                + "    @State\n"
                + "    public char mChar;\n"
                + "    @State\n"
                + "    public char[] mCharArray;\n"
                + "    @State\n"
                + "    public Character mCharObj;\n"
                + "    @State\n"
                + "    public double mDouble;\n"
                + "    @State\n"
                + "    public double[] mDoubleArray;\n"
                + "    @State\n"
                + "    public Double mDoubleObj;\n"
                + "    @State\n"
                + "    public float mFloat;\n"
                + "    @State\n"
                + "    public float[] mFloatArray;\n"
                + "    @State\n"
                + "    public Float mFloatObj;\n"
                + "    @State\n"
                + "    public int mInt;\n"
                + "    @State\n"
                + "    public int[] mIntArray;\n"
                + "    @State\n"
                + "    public Integer mIntegerObj;\n"
                + "    @State\n"
                + "    public long mLong;\n"
                + "    @State\n"
                + "    public long[] mLongArray;\n"
                + "    @State\n"
                + "    public Long mLongObj;\n"
                + "    @State\n"
                + "    public short mShort;\n"
                + "    @State\n"
                + "    public short[] mShortArray;\n"
                + "    @State\n"
                + "    public Short mShortObj;\n"
                + "    @State\n"
                + "    public CharSequence mCharSequence;\n"
                + "    @State\n"
                + "    public CharSequence[] mCharSequenceArray;\n"
                + "    @State\n"
                + "    public String mString;\n"
                + "    @State\n"
                + "    public String[] mStringArray;\n"
                + "    @State\n"
                + "    public ArrayList<CharSequence> mCharSequenceArrayList;\n"
                + "    @State\n"
                + "    public ArrayList<Integer> mIntegerArrayList;\n"
                + "    @State\n"
                + "    public ArrayList<String> mStringArrayList;\n"
                + "    @State\n"
                + "    public Bundle mBundle;\n"
                + "    @State\n"
                + "    public Parcelable[] mParcelableArray;\n"
                + "    @State\n"
                + "    public ParcelableImpl mParcelableImpl;\n"
                + "    @State\n"
                + "    public SerializableImpl mSerializableImpl;\n"
                + "    @State\n"
                + "    public ArrayList<ParcelableImpl> mParcelableArrayList;\n"
                + "    @State\n"
                + "    public SparseArray<ParcelableImpl> mParcelableSparseArray;\n"
                + "\n"
                + "    public static class ParcelableImpl implements Parcelable {\n"
                + "        private int mInt;\n"
                + "\n"
                + "        public ParcelableImpl(int anInt) {\n"
                + "            mInt = anInt;\n"
                + "        }\n"
                + "\n"
                + "        protected ParcelableImpl(Parcel in) {\n"
                + "            mInt = in.readInt();\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public boolean equals(Object o) {\n"
                + "            if (this == o) return true;\n"
                + "            if (o == null || getClass() != o.getClass()) return false;\n"
                + "\n"
                + "            ParcelableImpl that = (ParcelableImpl) o;\n"
                + "\n"
                + "            return mInt == that.mInt;\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public int hashCode() {\n"
                + "            return mInt;\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public void writeToParcel(Parcel dest, int flags) {\n"
                + "            dest.writeInt(mInt);\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public int describeContents() {\n"
                + "            return 0;\n"
                + "        }\n"
                + "\n"
                + "        public static final Creator<ParcelableImpl> CREATOR = new Creator<ParcelableImpl>() {\n"
                + "            @Override\n"
                + "            public ParcelableImpl createFromParcel(Parcel in) {\n"
                + "                return new ParcelableImpl(in);\n"
                + "            }\n"
                + "\n"
                + "            @Override\n"
                + "            public ParcelableImpl[] newArray(int size) {\n"
                + "                return new ParcelableImpl[size];\n"
                + "            }\n"
                + "        };\n"
                + "    }\n"
                + "\n"
                + "    public static class SerializableImpl implements Serializable {\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected = JavaFileObjects.forSourceString(getName(className, true), ""
                + "/* *****************************************************************************\n"
                + " * Copyright (c) 2017 Evernote Corporation.\n"
                + " * This software was generated by Evernote’s Android-State code generator\n"
                + " * (available at https://github.com/evernote/android-state), which is made\n"
                + " * available under the terms of the Eclipse Public License v1.0\n"
                + " * (available at http://www.eclipse.org/legal/epl-v10.html).\n"
                + " * For clarification, code generated by the Android-State code generator is\n"
                + " * not subject to the Eclipse Public License and can be used subject to the\n"
                + " * following terms:\n"
                + " *\n"
                + " * Permission is hereby granted, free of charge, to any person obtaining a copy\n"
                + " * of this software and associated documentation files (the \"Software\"), to deal\n"
                + " * in the Software without restriction, including without limitation the rights\n"
                + " * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n"
                + " * copies of the Software, and to permit persons to whom the Software is\n"
                + " * furnished to do so, subject to the following conditions:\n"
                + " *\n"
                + " * The above copyright notice and this permission notice shall be included in all\n"
                + " * copies or substantial portions of the Software.\n"
                + " *\n"
                + " * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
                + " * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
                + " * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
                + " * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n"
                + " * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n"
                + " * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
                + " * SOFTWARE.\n"
                + " *******************************************************************************/\n"
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.RuntimeException;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.lang.reflect.Field;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestTypesReflection$$StateSaver<T extends TestTypesReflection> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestTypesReflection$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mBundle\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putBundle(state, \"mBundle\", (android.os.Bundle) field.get(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mInt\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putInt(state, \"mInt\", (int) field.getInt(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mIntArray\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putIntArray(state, \"mIntArray\", (int[]) field.get(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mIntegerObj\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putBoxedInt(state, \"mIntegerObj\", (java.lang.Integer) field.get(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mParcelableArray\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putParcelableArray(state, \"mParcelableArray\", (android.os.Parcelable[]) field.get(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mParcelableArrayList\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putSerializable(state, \"mParcelableArrayList\", (java.io.Serializable) field.get(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mParcelableSparseArray\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      HELPER.putSparseParcelableArray(state, \"mParcelableSparseArray\", (android.util.SparseArray) field.get(target));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mBundle\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.set(target, HELPER.getBundle(state, \"mBundle\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mInt\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.setInt(target, HELPER.getInt(state, \"mInt\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mIntArray\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.set(target, HELPER.getIntArray(state, \"mIntArray\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mIntegerObj\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.set(target, HELPER.getBoxedInt(state, \"mIntegerObj\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mParcelableArray\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.set(target, HELPER.getParcelableArray(state, \"mParcelableArray\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mParcelableArrayList\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.set(target, HELPER.getSerializable(state, \"mParcelableArrayList\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "    try {\n"
                + "      Field field = target.getClass().getDeclaredField(\"mParcelableSparseArray\");\n"
                + "      boolean accessible = field.isAccessible();\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(true);\n"
                + "      }\n"
                + "      field.set(target, HELPER.getSparseParcelableArray(state, \"mParcelableSparseArray\"));\n"
                + "      if (!accessible) {\n"
                + "        field.setAccessible(false);\n"
                + "      }\n"
                + "    } catch (Exception e) {\n"
                + "      throw new RuntimeException(e);\n"
                + "    }\n"
                + "  }\n"
                + "}");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject, javaFileObjectInnerClass);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected);
    }

    @Test
    public void testInheritance() {
        String className = "TestInheritance";
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName(className), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestInheritance {\n"
                + "\n"
                + "    @State\n"
                + "    public int mValue1;\n"
                + "\n"
                + "    public static class InheritanceLevel1 extends TestInheritance {\n"
                + "\n"
                + "    }\n"
                + "\n"
                + "    public static class InheritanceLevel2 extends InheritanceLevel1 {\n"
                + "\n"
                + "        @State\n"
                + "        public int mValue2;\n"
                + "    }\n"
                + "}\n");

        JavaFileObject expected1 = JavaFileObjects.forSourceString(getName(className, true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import com.evernote.android.state.Injector;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestInheritance$$StateSaver<T extends TestInheritance> extends Injector.Object<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper(\"com.evernote.android.state.test.TestInheritance$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    HELPER.putInt(state, \"mValue1\", target.mValue1);\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    target.mValue1 = HELPER.getInt(state, \"mValue1\");\n"
                + "  }\n"
                + "}\n");

        JavaFileObject expected2 = JavaFileObjects.forSourceString(getName("TestInheritance$InheritanceLevel2", true), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.InjectionHelper;\n"
                + "import java.lang.Override;\n"
                + "import java.lang.String;\n"
                + "import java.lang.SuppressWarnings;\n"
                + "import java.util.HashMap;\n"
                + "\n"
                + "public class TestInheritance$InheritanceLevel2$$StateSaver<T extends TestInheritance.InheritanceLevel2> extends TestInheritance$$StateSaver<T> {\n"
                + "  private static final HashMap<String, Bundler<?>> BUNDLERS = new HashMap<String, Bundler<?>>();\n"
                + "\n"
                + "  private static final InjectionHelper HELPER = new InjectionHelper("
                + "\"com.evernote.android.state.test.TestInheritance$InheritanceLevel2$$StateSaver\", BUNDLERS);\n"
                + "\n"
                + "  static {\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void save(T target, Bundle state) {\n"
                + "    super.save(target, state);\n"
                + "    HELPER.putInt(state, \"mValue2\", target.mValue2);\n"
                + "  }\n"
                + "\n"
                + "  @Override\n"
                + "  @SuppressWarnings(\"unchecked\")\n"
                + "  public void restore(T target, Bundle state) {\n"
                + "    super.restore(target, state);\n"
                + "    target.mValue2 = HELPER.getInt(state, \"mValue2\");\n"
                + "  }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).succeeded();
        assertThat(compilation).generatedSourceFile(getName(className, true)).hasSourceEquivalentTo(expected1);
        assertThat(compilation).generatedSourceFile(getName("TestInheritance$InheritanceLevel2", true)).hasSourceEquivalentTo(expected2);
    }

    @Test
    public void testPrivateField() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestPrivateField"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestPrivateField {\n"
                + "    @State\n"
                + "    private int test;\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Field test must be either non-private or provide a getter and setter method")
                .inFile(javaFileObject)
                .onLine(7)
                .atColumn(17);
    }

    @Test
    public void testPrivateProperty() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestPrivateProperty"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestPrivateProperty {\n"
                + "    @State\n"
                + "    private int test;\n"
                + "\n"
                + "    private int getTest() {\n"
                + "        return test;\n"
                + "    }\n"
                + "\n"
                + "    public void setTest(int test) {\n"
                + "        this.test = test;\n"
                + "    }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Field test must be either non-private or provide a getter and setter method")
                .inFile(javaFileObject)
                .onLine(7)
                .atColumn(17);
    }

    @Test
    public void testPrivateClass() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestPrivateClass"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestPrivateClass {\n"
                + "    private static class Inner {\n"
                + "        @State\n"
                + "        public int test;\n"
                + "    }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Class must not be private")
                .inFile(javaFileObject)
                .onLine(6)
                .atColumn(20);
    }

    @Test
    public void testPrivateClassBundler() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestPrivateClassBundler"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestPrivateClassBundler {\n"
                + "    @State(MyBundler.class)\n"
                + "    private Data mData2;\n"
                + "\n"
                + "    public Data getData2() {\n"
                + "        return mData2;\n"
                + "    }\n"
                + "\n"
                + "    public void setData2(Data data2) {\n"
                + "        mData2 = data2;\n"
                + "    }\n"
                + "\n"
                + "    public static final class Data {\n"
                + "        private int int1;\n"
                + "        private int int2;\n"
                + "    }\n"
                + "\n"
                + "    private static final class MyBundler implements Bundler<Data> {\n"
                + "        @Override\n"
                + "        public void put(String key, Data value, Bundle bundle) {\n"
                + "            bundle.putInt(key + \"1\", value.int1);\n"
                + "            bundle.putInt(key + \"2\", value.int2);\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public Data get(String key, Bundle bundle) {\n"
                + "            Data data = new Data();\n"
                + "            data.int1 = bundle.getInt(key + \"1\");\n"
                + "            data.int2 = bundle.getInt(key + \"2\");\n"
                + "            return data;\n"
                + "        }\n"
                + "    }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Class must not be private")
                .inFile(javaFileObject)
                .onLine(25)
                .atColumn(26);
    }

    @Test
    public void testPrivateClassBundlerData() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestPrivateClassBundlerData"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import android.os.Bundle;\n"
                + "\n"
                + "import com.evernote.android.state.Bundler;\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestPrivateClassBundlerData {\n"
                + "    @State(MyBundler.class)\n"
                + "    private Data mData2;\n"
                + "\n"
                + "    public Data getData2() {\n"
                + "        return mData2;\n"
                + "    }\n"
                + "\n"
                + "    public void setData2(Data data2) {\n"
                + "        mData2 = data2;\n"
                + "    }\n"
                + "\n"
                + "    private static final class Data {\n"
                + "        private int int1;\n"
                + "        private int int2;\n"
                + "    }\n"
                + "\n"
                + "    public static final class MyBundler implements Bundler<Data> {\n"
                + "        @Override\n"
                + "        public void put(String key, Data value, Bundle bundle) {\n"
                + "            bundle.putInt(key + \"1\", value.int1);\n"
                + "            bundle.putInt(key + \"2\", value.int2);\n"
                + "        }\n"
                + "\n"
                + "        @Override\n"
                + "        public Data get(String key, Bundle bundle) {\n"
                + "            Data data = new Data();\n"
                + "            data.int1 = bundle.getInt(key + \"1\");\n"
                + "            data.int2 = bundle.getInt(key + \"2\");\n"
                + "            return data;\n"
                + "        }\n"
                + "    }\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Class must not be private")
                .inFile(javaFileObject)
                .onLine(20)
                .atColumn(26);
    }

    @Test
    public void testStatic() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestStatic"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestStatic {\n"
                + "    @State\n"
                + "    public static int test;\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Field must not be static")
                .inFile(javaFileObject)
                .onLine(7)
                .atColumn(23);
    }

    @Test
    public void testFinal() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestFinal"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestFinal {\n"
                + "    @State\n"
                + "    public final int test = 5;\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Field must not be final")
                .inFile(javaFileObject)
                .onLine(7)
                .atColumn(22);
    }

    @Test
    public void testTypeNotSupported() {
        JavaFileObject javaFileObject = JavaFileObjects.forSourceString(getName("TestTypeNotSupported"), ""
                + "package com.evernote.android.state.test;\n"
                + "\n"
                + "import com.evernote.android.state.State;\n"
                + "\n"
                + "public class TestTypeNotSupported {\n"
                + "    @State\n"
                + "    public Other test;\n"
                + "\n"
                + "    public static final class Other {}\n"
                + "}\n");

        Compilation compilation = Compiler.javac().withProcessors(new StateProcessor()).compile(javaFileObject);
        assertThat(compilation).failed();
        assertThat(compilation)
                .hadErrorContaining("Don't know how to put com.evernote.android.state.test.TestTypeNotSupported.Other into a bundle")
                .inFile(javaFileObject)
                .onLine(7)
                .atColumn(18);
    }
}
